本篇文章作為網站實戰開發的第七篇,我們將繼續實作更多細節,
在繼續往下開始之前,我們計畫先聊聊 View 的重用、模組化。
--------系列簡介--------
網站系統可說是現在最多學子與新人想要入門的一個領域,
這個原本屬於新興的領域,這幾年來也累積許多年的知識與 pattern ,
在有限的環境(stateless)與有限的伺服器端、瀏覽器資源下,
成為許多人進入程式的一塊入門鐵板(?)。
這個系列希望能夠就網站系統設計幾個門檻著手,
這是設定給初心者作為學習,給同好們作為回顧,
重新認識有關網站系統的每個環節。
在前三天的文章裡面,我們分別介紹了
1.如何引入 CI 、製作View
2.引入 bootstrap 協助我們進行 UI 設計
3.如何透過 CI 與 DB 進行互動,並做簡易的資料檢核、畫面呈現
由於我們主要是進行基礎教學,所以範例都會挑比較基本的實作,
接下來在我們繼續往下實作之前,本篇我們先聊一些更基本的事情:
@ View 的重複性:
雖然我們目前建置的 view 只有六個,其中只有兩個是有實作內容的,
但是其中已經有不少重複內容,如以註冊完成為例:
application/views/register_success.php
<html >
<meta charset="utf-8">
<title>發文系統 - 會員註冊成功</title>
<link rel="stylesheet" href="<?=base_url("/css/bootstrap.min.css")?>">
<link rel="stylesheet" href="<?=base_url("/css/bootstrap-responsive.min.css")?>">
<div class="container">
<div class="alert alert-success">
恭喜你 (<?=$account?>),你已經完成註冊,
接下來馬上到登入頁面去試試看吧!
<a href="<?=site_url("user/login")?>">登入</a>
</div>
</div>
其中你會發現,Head 的內容我們幾乎會在所有的 view 中都一直重複。
一般而言,有一種建構網頁規則的方式是將網頁分成:
* 共用性內容 (meta content)
常見於 css 樣式表的引入跟 title / description 以及一些常用全域 JS 的引入。(ex.jQuery )
* 目的性內容 (target content)
針對當前頁面要表達的內容,所需要的細節
通常要抽出多少共用性內容取決於頁面的複雜度跟邏輯上的需要,
但是一個頁面,通常都至少可以抽出 header 與 footer 兩個基本區塊。
如前述的內容,如果我將有關 header 載入的部份抽出並建立範例檔案,如下:
application/views/_site_header.php
<html >
<meta charset="utf-8">
<title><?php
if(isset($pageTitle)){
echo $pageTitle ; //透過變數設定
} else{
echo "發文系統" ; //預設標題
}
?></title>
<link rel="stylesheet" href="<?=base_url("/css/bootstrap.min.css")?>">
<link rel="stylesheet" href="<?=base_url("/css/bootstrap-responsive.min.css")?>">
(PS:底線開頭是筆者的命名習慣,意味著這個 php 只能被用於載入。)
另外雖然 footer 目前沒有太多資料,但暫時也先抽出如:
application/views/_site_footer.php
切 footer 的優點在於全站性 GA (Google Analytics) 設定會輕鬆很多,
這是很常見得需求。
這麼一來,我們原本的 register_success.php 這個 View 就可以大幅簡化為
<?php include("_site_header.php"); ?>
<div class="container">
<div class="alert alert-success">
恭喜你 (<?=$account?>),你已經完成註冊,
接下來馬上到登入頁面去試試看吧!
<a href="<?=site_url("user/login")?>">登入</a>
</div>
</div>
<?php include("_site_footer.php"); ?>
@ include ?
include 是 php 用來載入另一個 view 的語法,他會先載入指定的 php 執行完畢,
並將指定的 php 所產出的 html 繪出後再繼續處理原本的 php 檔案。
@ 為什麼要用 include
這樣可以讓我們更聚焦於內容的區塊,因為大部分的 head 基本上都是重複的,
而沒有重複的地方(如頁面標題),我們可以透過定義變數的方式進行處理。
在這裡我們還需要設定原本的 user controller ,
將 title 改為 "發文系統 - 會員註冊成功",步驟如下:
修改 application/controllers/user.php
$this->load->view('register_success',Array(
"account" => $account,
"pageTitle" => "發文系統 - 會員註冊成功" //設定 pageTitle 參數
));
完整修改後的檔案清單請參考這個網址
@ isset ?
這個 php 的函數我們一直都還沒有介紹到,這裡指的意思是判斷這個變數是否有被定義,
如果我們不確定這個變數一定會被定義到,(如view可能不一定每次都會寫)
希望這個變數在沒有指定狀況也會動,就會需要這個判斷。
if(isset($myVar)){ //有定義 $myVal 的情況下
}else{
//沒有定義 myVal 的情況下
}
@ 接著我們來套用其他頁面吧,如 register view :
套用前
<html >
<meta charset="utf-8">
<title>發文系統 - 會員註冊</title>
<link rel="stylesheet" href="<?=base_url("/css/bootstrap.min.css")?>">
<link rel="stylesheet" href="<?=base_url("/css/bootstrap-responsive.min.css")?>">
<div class="container">
<form action="<?=site_url("/user/registering")?>" method="post" >
<?php if (isset($errorMessage)){?>
<div class="alert alert-error">
<?=$errorMessage?>
</div>
<?php }?>
<table class="table table-bordered">
<tr>
<td>
Account
</td>
<td>
<?php if(isset($account)){ ?>
<input type="text" name="account" value="<?=htmlspecialchars($account)?>" />
<?php }else{ ?>
<input type="text" name="account" />
<?php } ?>
</td>
</tr>
<tr>
<td>
Password
</td>
<td>
<input type="password" name="password" />
</td>
</tr>
<tr>
<td>
Re-type Password
</td>
<td>
<input type="password" name="passwordrt" />
</td>
</tr>
<tr>
<td colspan="2">
<input class="btn" type="submit" value="送出" />
</td>
</tr>
</table>
</form>
</div>
套用後:
<?php include("_site_header.php"); ?>
<div class="container">
<form action="<?=site_url("/user/registering")?>" method="post" >
<?php if (isset($errorMessage)){?>
<div class="alert alert-error">
<?=$errorMessage?>
</div>
<?php }?>
<table class="table table-bordered">
<tr>
<td>
Account
</td>
<td>
<?php if(isset($account)){ ?>
<input type="text" name="account" value="<?=htmlspecialchars($account)?>" />
<?php }else{ ?>
<input type="text" name="account" />
<?php } ?>
</td>
</tr>
<tr>
<td>
Password
</td>
<td>
<input type="password" name="password" />
</td>
</tr>
<tr>
<td>
Re-type Password
</td>
<td>
<input type="password" name="passwordrt" />
</td>
</tr>
<tr>
<td colspan="2">
<input class="btn" type="submit" value="送出" />
</td>
</tr>
</table>
</form>
</div>
<?php include("_site_footer.php"); ?>
另外當然不免俗的還是要修改 controller/user.php
public function register()
{
$this->load->view('register',
Array("pageTitle" => "發文系統 - 會員註冊")
);
}
再比對一次修改前與修改後結果,有沒有覺得更能聚焦在重要的細節上了呢?
另外由 controller 決定標題也是更適合其責任的作法,
像是瀏覽文章頁面時,我們可能會希望以文章的標題作為標題,
讀取作者頁面時,我們可能希望讀取作者的帳號作為標題的一部分。
@ 在我們繼續後面的討論之前
在這裡通常是很容易有爭議的部份,所以我們得用多些章節來解釋。
在這裡我們必須再回頭探討一下我們這樣設計的幾個假設,
首先我們的第一個假設是使用者會隨著頁面切換 css 檔案、head 區塊的機率不大。
@ Head ?
這裡你可能會有異見,因為很多人所學的設計模式,
就是 JavaScript file 應該放置於 Head,
而 head 會需要不斷的根據頁面所使用到的 JavaScript 去載入不同 JS。
OK,基本上筆者並不認同這種設計方案,
對於 JS 引入的設計方案,筆者認為最最理想的狀況,
應該是透過 AMD 的作法只載入一隻 main 的 JS ,並且由其進行後續的相依性處理。
這樣就可以完全使用一個變數,就解決大多的問題。
當然這個目前還不是非常主流的設計方案,
因為開發者對 AMD 的認識還不足,加上還有許多舊專案在這個世界上。
但至少你真正應該做的,是將 JavaScript 檔案放置 site_footer 之前載入,
在 Head 載入 JavaScript 在大多數一般的狀況下沒有具體的益處,
而且有許多的壞處,像是會使頁面輸出中止,而讓頁面讀取感覺非常緩慢。
一般 JS 的設計與效能調校教材,都會教你把 JS 的讀取與執行放置在 之前。
@ 那不放 JS, CSS 總要變化了吧?
一般而言,筆者是蠻鼓勵大家把 CSS 減少檔案數並且盡量一次載入的,
理由是檔案變多連線數就會變多,會造成連線上的效率低落。
如筆者的習慣會是:
1.site.css 負責放全站所有的 css ,其中不同頁面用到的 css ,
用不同的 namespace 與註解清楚分隔開來供日後修改參考。
2.其他 plugin css 將需要預先載入的 css ,直接預設就讓他載入。
(如果是 tinyMCE 或 ckeditor 這種狀況的特例處理,讓他自己讀取就好。:P )
@ 模組化:不重複、彈性、可讀性
在我們繼續後面的討論之前,最後再談有關切分子畫面可能的爭議,
基本上核心原則就是不要有重複一次以上的代碼。
但是這裡的重複,筆者認為該考量得是精神上的重複,而不是實質上的重複,
舉例,<div> 這個 tag 重複無數次,我們需要為他建立 helper 來避免 <div> 重複嗎?
不,應該要看 <div> 內容要做的目的是否重複。
做這種 no duplicated code 時,筆者希望讀者們有件事情一定要謹記於心:
很多東西看起來很像,但內容與精神其實是完全不一樣的東西。
這種時候是應該按照精神跟內容去分類的。
另外因為要模組化,其中有部份需要變動時,我們就會需要透過變數來進行處理,
如何使變數的數量處於一個合理而不擾人的狀況,也是一個課題。
(當用一個view要一直想有哪些變數要給,是很討厭的。)
一般而言筆者會將變數以註解的方式標明於 view 的最上方,方便取用的人進行瞭解。
第三件事情是如果切分太多的子 view,特別是子 view 裡面又有子 view 時,
這時候程式碼的可讀性,將會因為層層遮蔽而更難以瞭解全貌。
可能的話,要盡可能避免複雜的子層結構,讓 view 只是個簡單的 view。
基本上切分 view 並沒有所謂的準則,像 CI 內建也有提供 layout 讓你使用,
但是筆者對 layout 全站只能有一個這件事情不太滿意,所以並不常用。XD
如何切分 module ,並且讓網站因此組織得很漂亮,
對筆者而言並沒有真正的 best practice ,端看於頁面功能數、結構複雜程度。
這裡說得只是筆者常使用的 pattern 之一,但是也常常會因地制宜做出不同設計,
希望讀者可以在之後自行去體會這其中的細節與奧妙。:)
好的,再經過一堆囉唆的介紹之後,重要的當然還是實戰。
技術這種東西的討厭點在於,明明寫起來跟組裝起來就是很簡單的事情,
扯到「觀念」這回事就非得寫得落落長,深怕一個寫錯就會讓人誤入歧途。XD
只有程式碼是不會騙人的,也只有撰寫程式碼才能累積實戰的細節。:P
好的,接下來我們繼續開發這樣一個網站,首先我們先稍微回顧現在的狀態,
我們要做的事情是大約以下的範圍:
* 首頁
* 熱門文章
* 熱門作者
* 會員系統
* 註冊
* 註冊完成
* 註冊失敗(資料錯誤)
* 登入
* 登入失敗密碼輸入錯誤
* 忘記密碼表單
* 忘記密碼流程相關頁面
* 登入成功(轉向、訊息提示、畫面回饋)
* 文章管理系統
* 瀏覽
* 作者所有文章列表
* 作者分類文章列表
* 單一文章頁面
* 全站分類列表
* 全站分類文章清單
* 發表
* 文章發表
* 發表失敗(字數超出限制、使用者已登出等)
* 文章編輯
* 編輯失敗(字數超出限制、使用者已登出等)
* 文章刪除
* 刪除失敗(使用者已登出、使用者權限不符等)
但我們現在進行的只有底下的部份:
* 會員系統
* 註冊
* 註冊完成
* 註冊失敗(資料錯誤)
接下來我們即將面臨每個專案都會碰到的問題,要先做哪一個。XD
先解釋為什麼我們先挑註冊,當時先挑註冊,
是因為沒有註冊就沒有會員,沒有會員的狀況下發文章很奇怪。:P
雖然說也是可以先從資料庫建立 user 資料,
來先跳過註冊階段開發後續,但就寫文章而言這樣解釋起來太麻煩。:P
一般筆者實作這類功能時,主要會先從以下順序進行:
1.首頁(只有主要 navigation ,不含熱門文章、熱門作者)
2.會員註冊(不含修改密碼)
3.會員登入
4.文章發表
5.文章瀏覽
6.文章編輯
7.文章刪除
8.首頁(熱門作者、熱門文章)
9.會員註冊(修改密碼)
會這麼做是因為要按照功能的重要權重進行,
假設今天你在做一個專案,重要得是整體的完整性而非局部的完整性。
如果你有熱門文章,但是沒有可以瀏覽文章的頁面,這樣也是 fail,
但是如果你有瀏覽文章頁面,但是沒有熱門文章頁面,這樣還有救。XD
簡言之,比起局部徹底做好一個子部位,瞄準全站重要的核心細節先做完,
之後再回來補剩下 second priority 的功能是更理想的作法。
這也是筆者跑專案經驗中跟其他人比較不同的作法。
因為我怕這篇不小心就會突破本日的文章字數上限,
所以我們就快速按照這個順序實作完吧,
照慣例,先寫 code 後解釋,今天解釋不完的就明天解釋。:)
1.首頁
這裡我們需要實作的有幾個部份,
我們先參考底下這張之前畫過的 wireframe 圖。
我們會按照常見的設計慣例在上方有幾個連結可以回到首頁,
右上角有登入狀態及登入鈕,而登入後可能會出現 My Articles 這樣,
所以我們要先為首頁進行設計,首頁的 view 根據我們之前的範例,
是 application/views/welcome_message.php。
* 套入 _site_head.php 、 _site_footer.php
<?php include("_site_header.php"); ?>
<div class="container">
這是首頁內容,尚未進行內容設計。
</div>
<?php include("_site_footer.php"); ?>
* 實作我們需要的上方連結,這裡我們直接使用 bootstrap nav 幫助我們快速開發。:)
<?php include("_site_header.php"); ?>
<div class="container home">
<!-- Content Header -->
<div class="content-header">
<div class="navbar navbar-inverse">
<div class="navbar-inner">
<a class="brand" href="<?=site_url("/")?>">The Articles</a>
<ul class="nav">
<li class="active"><a href="#">Home</a></li>
<li><a href="#">My Articles</a></li>
</ul>
<!-- login status -->
<ul class="nav pull-right">
<li><a href="<?=site_url("user/login")?>">登入</a></li>
<li class="divider-vertical"></li>
<li><a href="<?=site_url("user/register")?>">註冊</a></li>
</ul>
</div>
</div>
</div><!-- content -->
<div class="content">
<div>
熱門文章(待實作)
</div>
<div>
熱門作者(待實作)
</div>
</div>
</div>
<?php include("_site_footer.php"); ?>
實作結果如下圖:
有句話說,像不像、三分樣。
有沒有開始覺得比較像樣一些了?
@ 再一次進行模組化
眼尖的讀者應該會發現,這條導覽列(navigation bar) 應該會出現在很多地方,
所以這時候我們也要抽 _content_nav.php 囉!
application/views/_content_nav.php
<!-- Content Header -->
<div class="content-header">
<div class="navbar navbar-inverse">
<div class="navbar-inner">
<a class="brand" href="<?=site_url("/")?>">The Articles</a>
<ul class="nav">
<li class="active"><a href="#">Home</a></li>
<li><a href="#">My Articles</a></li>
</ul>
<!-- login status -->
<ul class="nav pull-right">
<li><a href="<?=site_url("user/login")?>">登入</a></li>
<li class="divider-vertical"></li>
<li><a href="<?=site_url("user/register")?>">註冊</a></li>
</ul>
</div>
</div>
</div>
而我們的首頁 (welcome_message.php) 也就對應調整為:
<?php include("_site_header.php"); ?>
<div class="container home">
<?php include("_content_nav.php") ?>
<!-- content -->
<div class="content">
<div>
熱門文章(待實作)
</div>
<div>
熱門作者(待實作)
</div>
</div>
</div>
<?php include("_site_footer.php"); ?>
接著我們可能會希望全站的畫面都有這個 navigation bar,
所以我們回頭調整 register_success.php / register.php 。
因為筆者怕一萬字的文章字數塞不下所有程式碼,
所以程式碼請參考這個連結:https://gist.github.com/3888869
馬上來瀏覽修改完的結果:
1.註冊頁:
2.註冊完成頁:
有沒有覺得加上一致的 navigation bar 之後,
更有一體性,更像網站了呢?
由於今天字數即將爆表,所以就先不往下寫了,
明天的文章裡面我們會接著實作完這個發文系統更多細節。
文末再寫一些有關視覺樣板設計的部份,
就純功能頁面轉換成經過設計的視覺頁面相對而言是輕鬆的,
但前提是設計時就不能對 UI 有太多假設。
像是你不能假設某個 html 元素一定在另一個 html 元素之後,
因為經過 UI 設計之後,很多事情都會變得很不一樣。
這裡你會發現我們是寫完註冊之後,才回頭寫首頁並建立 navigation 樣板,
這基本上是因為撰文說明上的設定,真正開發時不一定會這麼做。
開發時基本上有一個原則就是共用的東西最好能先準備好就先準備好,
所以像這種狀況下,一般我們會先實作首頁,
把一些可能共用的東西先抽出來,像 navigation bar、登入部份,
實作子頁面時也會優先以結構上最具代表性者為優先。
當然在實作子頁面時,心中要先有整體的網站結構圖,
才不會有見樹不見林的狀況。
像是明明應該寫成共用的元件,卻寫成只有這個頁面能用,
其他頁面使用時會發生困難的狀況,這樣就是非常不理想的結果。
筆者一開始設計網站系統時,往往就是頁面跟頁面切得很開,
感覺上就像是獨立頁面,沒有一體性,
就程式碼維護上時也常常有子系統發生不一致的狀況。
之後多次碰到修改邏輯,如要修改上方 navigation bar 的連結內容,
有修改上的疏漏或者時間上的無謂消耗,
因為切身體驗過這些設計上的錯誤,所以更重視共用與模組。
當然共用與模組也是有困擾的,也發生過因為把頁面切得太細跟因為共用頁面,
導致明明只是想修改 A 功能的某個規則,沒有仔細考慮好相依性,
結果連 B、C、D、E 功能的規則也被改變的狀況。
過猶不及都是困擾,這中間的尺如何拿捏,只有「經驗」跟「溝通」能成火侯。
我們目前前天、昨天、今天的內容有一點脫出原本的大綱,
這點是當初規劃大綱時沒有規劃好的,所以我們當然要視實際進行的結果修正囉。
這是原本的計畫:
17.視覺 vs 資料,談從資料表到畫面
18.資料表與功能共舞 :「美麗的巧合」
19.如何保衛你的資料,談那些你沒有想到的情境
20.你的網站會有幾種使用者?初談使用者角色
21.網站以外的程式,談 cron jobs 、smtp、mail sender 等網站伺服器以外的服務。
所以我們將做出對應的調整是將 17-18-19 這三篇再加上三篇,
也就是實際上的 17-18-19-20-21-22 這幾篇,內容為「網站實戰開發」。
而 23 - 24 則調整為原本的「談使用者角色」跟「網站以外的程式」,
之後接著六篇前端開發概論,原本的第五週就不談了這樣。:)
筆者認為實戰開發才是最重要的核心基礎細節,
其他的部份可以之後有機會再令外傳文討論。
明天,我們將繼續實作登入狀態與發文相關的細節,那麼,明天見囉。;)